package com.simpou.commons.utils.lang;

import com.simpou.commons.utils.behavior.RandGenerator;
import com.simpou.commons.utils.behavior.Randomizable;
import com.simpou.commons.utils.behavior.Randomizer;
import com.simpou.commons.utils.cipher.Hashs;
import com.simpou.commons.utils.lang.annot.RandomFillableConfig;
import com.simpou.commons.utils.model.MultipleObjects;
import com.simpou.commons.utils.pagination.PageLimits;
import com.simpou.commons.utils.reflection.Casts;
import com.simpou.commons.utils.reflection.Generics;
import com.simpou.commons.utils.reflection.Reflections;
import com.simpou.commons.utils.validation.Assertions;

import java.io.Serializable;
import java.math.BigDecimal;
import java.util.*;


/**
 * Gerador de dados aleatórios.
 *
 * @author Jonas Pereira
 * @version 2012-10-17
 * @since 2010-07-28
 */
public class Randoms implements Serializable {
    /**
     * Máximo de algarismos de uma string numérica.
     */
    public static final int MAX_STRING_NUMBER_CHARS = 9;

    /**
     * Máximo valor de um número que compõe um endereço IP.
     */
    private static final int MAX_IP_ITEM = 255;

    /**
     * Número máximo de caracteres de uma string.
     */
    public static final int MAX_STRING_CHARS = 300;

    /**
     * Gerador de dados aleatórios.
     */
    private static final Random RANDOM;

    static {
        RANDOM = new Random();
        RANDOM.setSeed(new GregorianCalendar().getTimeInMillis() +
                System.nanoTime());
    }

    /**
     * @return Byte.
     */
    public static Byte getByte() {
        byte[] bytes = new byte[1];
        RANDOM.nextBytes(bytes);

        return bytes[0];
    }

    /**
     * @return Short.
     */
    public static Short getShort() {
        return getInteger().shortValue();
    }

    /**
     * @return Long.
     */
    public static Long getLong() {
        return RANDOM.nextLong();
    }

    /**
     * @return Double.
     */
    public static Double getDouble() {
        double value = getInteger();

        return value + getFloat();
    }

    /**
     * <p>getIP.</p>
     *
     * @return Endereço IP v4.
     */
    public static String getIP() {
        int p1 = getInteger(0, MAX_IP_ITEM);
        int p2 = getInteger(0, MAX_IP_ITEM);
        int p3 = getInteger(0, MAX_IP_ITEM);
        int p4 = getInteger(0, MAX_IP_ITEM);

        return p1 + "." + p2 + "." + p3 + "." + p4;
    }

    /**
     * @return String com número de caracteres entre 1 e 10, não incluindo
     *         caracteres especiais.
     */
    public static String getString() {
        return getString(1, 10, false);
    }

    /**
     * <p>getString.</p>
     *
     * @param length          Número de caracteres.
     * @param includeEspecial Define se irá incluir caracteres especiais.
     * @return String com número de caracteres fixo.
     */
    public static String getString(final int length,
                                   final boolean includeEspecial) {
        Assertions.inRange(length, 0, MAX_STRING_CHARS);

        final StringBuilder buffer = new StringBuilder();

        for (int i = 0; i < length; i++) {
            buffer.append(getChar(includeEspecial));
        }

        return buffer.toString();
    }

    /**
     * <p>getString.</p>
     *
     * @param minLength       Mínimo número de caracteres.
     * @param maxLength       Máximo número de caracteres.
     * @param includeEspecial Define se irá incluir caracteres especiais.
     * @return String com número de caracteres limitado.
     */
    public static String getString(final int minLength, final int maxLength,
                                   final boolean includeEspecial) {
        final int length = getInteger(minLength, maxLength);

        return getString(length, includeEspecial);
    }

    /**
     * @return Hash 256 aleatório.
     */
    public static String getSHA256() {
        final String string = getString();

        try {
            return Hashs.getHash(string);
        } catch (Exception ex) {
            return getString(64, 64, false);
        }
    }

    /**
     * <p>getString.</p>
     *
     * @param minLength       Mínimo número de caracteres.
     * @param maxLength       Máximo número de caracteres.
     * @param includeEspecial Define se irá incluir caracteres especiais.
     * @param allowNull       Define se retorno nulo é aceitável.
     * @param allowEmpty      Define se retorno vazio é aceitável.
     * @return String com número de caracteres limitado, string nula ou vazia.
     */
    public static String getString(final int minLength, final int maxLength,
                                   final boolean includeEspecial, final boolean allowNull,
                                   final boolean allowEmpty) {
        if (allowNull && getBoolean()) {
            return null;
        } else if (allowEmpty && getBoolean()) {
            return "";
        } else {
            return getString(minLength, maxLength, includeEspecial);
        }
    }

    /**
     * Gera uma string cujo número de caracteres pode ser: menor que o limite
     * inferior, maior que o limite superior ou nulo.
     * <p/>
     * Máximo de 300 caracteres são retornados.
     *
     * @param lowerLimit      Limite inferior. Valor negativo para ignorar.
     * @param upperLimit      Limite superior. Valor negativo para ignorar.
     * @param includeEspecial Define se irá incluir caracteres especiais.
     * @param allowNull       Define se retorno nulo é aceitável.
     * @return String com número de caracteres limitado.
     */
    public static String getString(final int lowerLimit, final int upperLimit,
                                   final boolean includeEspecial, final boolean allowNull) {
        if (upperLimit >= MAX_STRING_CHARS) {
            throw new IllegalArgumentException("Maximum upper limit allowed: " +
                    (MAX_STRING_CHARS - 1) + ", given " + upperLimit);
        }

        Integer length = getInteger(lowerLimit, upperLimit, allowNull);

        if (length == null) {
            return null;
        } else {
            length = Math.min(length, MAX_STRING_CHARS);
            length = Math.max(length, 1);

            return getString(length, includeEspecial);
        }
    }

    /**
     * <p>getStringNumber.</p>
     *
     * @param length Número de algarismos. Máximo: 9.
     * @return String numérico com número de algarismos determinado.
     */
    public static String getStringNumber(final int length) {
        verifyMinMax(1, MAX_STRING_NUMBER_CHARS, length);

        StringBuilder buffer = new StringBuilder();

        buffer.append(getInteger(1, 9));

        for (int i = 0; i < (length - 1); i++) {
            buffer.append(getDigit());
        }

        return buffer.toString();
    }

    /**
     * <p>getStringNumber.</p>
     *
     * @param minLength Mínimo número de algarismos.
     * @param maxLength Máximo número de algarismos. Máximo: 9.
     * @return String numérico com número de algarismos limitado.
     */
    public static String getStringNumber(final int minLength,
                                         final int maxLength) {
        final int n = getInteger(minLength, maxLength);

        return getStringNumber(n);
    }

    /**
     * <p>getNumber.</p>
     *
     * @param minAlgs Mínimo número de algarismos.
     * @param maxAlgs Máximo número de algarismos. Máximo: 9.
     * @return Número com número de algarismos determinado.
     */
    public static int getNumber(final int minAlgs, final int maxAlgs) {
        final int algs = getInteger(minAlgs, maxAlgs);

        return getNumber(algs);
    }

    /**
     * <p>getNumber.</p>
     *
     * @param algs Número de algarismos. Máximo: 9.
     * @return Nnúmero com número de algarismos fixo.
     */
    public static int getNumber(final int algs) {
        return Integer.parseInt(getStringNumber(algs));
    }

    /**
     * <p>getDigit.</p>
     *
     * @return Número inteiro de 0 a 9.
     */
    public static int getDigit() {
        return getPositiveInt() % 10;
    }

    /**
     * <p>getFloat.</p>
     *
     * @return Float.
     */
    public static float getFloat() {
        return (float) (RANDOM.nextFloat() * Math.pow(10, getInteger(0, 5)));
    }

    /**
     * <p>getBigDecimal.</p>
     *
     * @param minValue Máximo valor.
     * @param maxValue Mínimo valor.
     * @return Bigdecimal.
     */
    public static BigDecimal getBigDecimal(final long minValue,
                                           final long maxValue) {
        return new BigDecimal(getInteger((int) minValue, (int) maxValue));
    }

    /**
     * @return BigDecimal.
     */
    public static BigDecimal getBigDecimal() {
        return new BigDecimal(getDouble());
    }

    /**
     * @return Inteiro.
     */
    public static Integer getInteger() {
        return RANDOM.nextInt();
    }

    /**
     * @param minValue Máximo valor.
     * @param maxValue Mínimo valor.
     * @return Número inteiro.
     */
    public static Integer getInteger(final int minValue, final int maxValue) {
        Assertions.greaterThan(maxValue, minValue - 1);

        int i = getPositiveInt();
        i = minValue + (i % (maxValue - minValue + 1));

        return i;
    }

    /**
     * Gera um número decimal não negativo cujo valor seja menor que o limite
     * inferior ou maior que o limite superior.
     *
     * @param lowerLimit Limite inferior. Se negativo limite será ignorado.
     * @param upperLimit Limite superior. Se negativo limite será ignorado.
     * @param allowNull  Define se retorno nulo é aceitável.
     * @return Número limitado.
     */
    public static Integer getInteger(final int lowerLimit,
                                     final int upperLimit, final boolean allowNull) {
        if ((lowerLimit == 0) || (upperLimit == 0)) {
            throw new IllegalArgumentException("Limits must be positive " +
                    "or negative.");
        }

        Integer decimal;

        if (upperLimit > 0) {
            if (lowerLimit > 0) {
                // limitação inferior e superior
                Assertions.greaterThan(upperLimit, lowerLimit - 1);

                if (getBoolean()) {
                    decimal = getInteger(0, lowerLimit - 1);
                } else {
                    decimal = getInteger(upperLimit + 1, Integer.MAX_VALUE);
                }
            } else {
                // limitação superior
                decimal = getInteger(upperLimit + 1, Integer.MAX_VALUE);
            }
        } else {
            if (lowerLimit > 0) {
                // limitação inferior
                decimal = getInteger(0, lowerLimit - 1);
            } else {
                // sem limitação, apenas null é válido
                if (allowNull) {
                    decimal = null;
                } else {
                    throw new IllegalArgumentException(
                            "Cannot generate a valid " +
                                    "value, only NULL allowed with all limits negative.");
                }
            }
        }

        return decimal;
    }

    /**
     * <p>getLong.</p>
     *
     * @param minValue Máximo valor.
     * @param maxValue Mínimo valor.
     * @return Número Long.
     */
    public static Long getLong(final int minValue, final int maxValue) {
        return Long.valueOf(getInteger(minValue, maxValue));
    }

    /**
     * <p>getChar.</p>
     *
     * @param includeEspecial Define o caracter pode ser especial.
     * @return Char.
     */
    public static char getChar(final boolean includeEspecial) {
        int i;

        // da tabela ascii
        if (includeEspecial) {
            i = getInteger(33, 126);
        } else {
            i = getInteger(97, 122);
        }

        return (char) i;
    }

    /**
     * <p>getPositiveInt.</p>
     *
     * @return Inteiro maior ou igual a zero.
     */
    public static int getPositiveInt() {
        int nextInt = RANDOM.nextInt();

        if (nextInt < 0) {
            nextInt *= -1;
        }

        return nextInt;
    }

    /**
     * <p>getBoolean.</p>
     *
     * @return Booleano.
     */
    public static boolean getBoolean() {
        return RANDOM.nextBoolean();
    }

    /**
     * <p>getDate.</p>
     *
     * @return Data entre os anos de 1950 e 3000.
     */
    public static Date getDate() {
        return new GregorianCalendar(getInteger(1950, 3000), getInteger(1, 12),
                getInteger(0, 60), getInteger(0, 23), getInteger(0, 59)).getTime();
    }

    /**
     * @param start Data de início.
     * @param end   Data de final, deve ser posterior ao início.
     * @return Data entre os limites inclusive.
     */
    public static Date getDate(Date start, Date end) {
        long time = Assertions.greaterThan(end.getTime() - start.getTime(), 0L,
                "End must be after start.");
        time = start.getTime() + (long) (time * RANDOM.nextFloat());

        return new Date(time);
    }

    /**
     * <p>getDate.</p>
     *
     * @param future Define se deve ser futuro ou passado.
     * @return Date.
     */
    public static Date getDate(final boolean future) {
        long dateLong = new GregorianCalendar().getTimeInMillis();
        long offset = getInteger(1000000, 10000000);

        if (future) {
            dateLong += offset;
        } else {
            dateLong -= offset;
        }

        return new Date(dateLong);
    }

    /**
     * <p>getEmail.</p>
     *
     * @return E-mail válido.
     */
    public static String getEmail() {
        return getEmail(getBoolean());
    }

    /**
     * <p>getEmail.</p>
     *
     * @param singleSufix True para um sufixo, false para dois.
     * @return E-mail válido.
     */
    public static String getEmail(final boolean singleSufix) {
        String prefix = getString(1, 5, false);
        String sufix1 = getString(1, 5, false);
        String sufix2 = getString(2, 3, false);
        String email = prefix + "@" + sufix1 + "." + sufix2;

        if (singleSufix) {
            email += ("." + getString(2, 3, false));
        }

        return email;
    }

    /**
     * <p>getEnum.</p>
     *
     * @param <T>   Tipo do enum.
     * @param clasz Classe do enum.
     * @return Enum.
     */
    public static <T> T getEnum(final Class<T> clasz) {
        Enum[] enums = (Enum[]) clasz.getEnumConstants();
        int i = getPositiveInt() % enums.length;

        return (T) enums[i];
    }

    /**
     * <p>getEnums.</p>
     *
     * @param <T>   Tipo do enum.
     * @param clasz Classe do enum.
     * @return Lista de enums.
     */
    public static <T> List<T> getEnums(final Class<T> clasz) {
        Enum[] enums = (Enum[]) clasz.getEnumConstants();
        List<T> list = new ArrayList<T>();

        for (int i = 0; i < enums.length; i++) {
            if (getBoolean()) {
                list.add((T) getEnum(clasz));
            }
        }

        return list;
    }

    /**
     * <p>getSingle.</p>
     *
     * @param <T>  Tipo do elemento.
     * @param list Lista.
     * @return Um elemento de uma lista.
     */
    public static <T> T getSingle(final Collection<T> list) {
        T[] tsAux = (T[]) new Object[0]; //evita warn de classCast no PMD
        T[] ts = list.toArray(tsAux);

        return getSingle(ts);
    }

    /**
     * @param clasz Classe do objeto a ser instanciado.
     * @return Nova instância da classe preenchida randomicamente.
     */
    public static <T extends Randomizable> T getSingle(final Class<T> clasz) {
        final T instance = Reflections.newInstance(clasz);
        instance.fillRandom();
        return instance;
    }

    /**
     * @param clasz      Classe do objeto a ser instanciado.
     * @param randomizer Preenchedor randômico.
     * @return Nova instância da classe preenchida randomicamente.
     */
    public static <T> T getSingle(final Class<T> clasz, final Randomizer<T> randomizer) {
        final T instance = Reflections.newInstance(clasz);
        randomizer.fillRandom(instance);
        return instance;
    }

    /**
     * Obtém uma instância de RandomFillable sem exigir, durante a compilação,
     * que a classe seja do tipo compatível. Em tempo de execução exceções
     * poderão ser lançadas em caso de incompatibilidade, por isso, use com
     * cautela.
     *
     * @param clasz Classe do objeto a ser instanciado.
     * @return Nova instância da classe preenchida randomicamente.
     * @throws ClassCastException Se classe for do tipo RandomFillable.
     */
    public static <T> T getSingleIgnoreType(final Class<T> clasz) {
        if (!Randomizable.class.isAssignableFrom(clasz)) {
            throw new ClassCastException("Class is not a " +
                    Randomizable.class.getName());
        }

        Class<? extends Randomizable> randClass = Casts.simpleCast(clasz);
        Randomizable single = getSingle(randClass);

        return Casts.objCast(clasz, single);
    }

    /**
     * <p>getSingle.</p>
     *
     * @param <T>  Tipo do elemento.
     * @param list Lista.
     * @return Um elemento de uma lista.
     */
    public static <T> T getSingle(final T[] list) {
        int length = list.length;
        int choose = chooseNumber(length);

        if (choose < 0) {
            return null;
        } else {
            return list[choose];
        }
    }

    /**
     * Obtém uma lista de instâncias de RandomFillable sem exigir, durante a
     * compilação, que a classe seja do tipo compatível. Em tempo de execução
     * exceções poderão ser lançadas em caso de incompatibilidade, por isso, use
     * com cautela.
     *
     * @param <T>   Tipo do objeto.
     * @param clasz Classe do objeto.
     * @param min   Número mínimo de elementos.
     * @param max   Número máximo de elementos.
     * @return Lista de objetos. classe.
     */
    public static <T> List<T> getListIgnoreType(final Class<T> clasz,
                                                final int min, final int max) {
        final int size = getInteger(min, max);
        final List<T> ts = new ArrayList<T>();

        for (int i = 0; i < size; i++) {
            ts.add(getSingleIgnoreType(clasz));
        }

        return ts;
    }

    /**
     * <p>getList.</p>
     *
     * @param <T>   Tipo do objeto.
     * @param clasz Classe do objeto.
     * @param min   Número mínimo de elementos.
     * @param max   Número máximo de elementos.
     * @return Lista de objetos. classe.
     */
    public static <T extends Randomizable> List<T> getList(
            final Class<T> clasz, final int min, final int max) {
        final int size = getInteger(min, max);
        final List<T> ts = new ArrayList<T>();

        for (int i = 0; i < size; i++) {
            ts.add(getSingle(clasz));
        }

        return ts;
    }

    /**
     * <p>getList.</p>
     *
     * @param <T>        Tipo do objeto.
     * @param clasz      Classe do objeto.
     * @param randomizer Preenchedor randômico.
     * @return Lista de objetos. classe.
     * @see com.simpou.commons.utils.lang.annot.RandomFillableConfig
     */
    public static <T> List<T> getList(
            final Class<T> clasz, final Randomizer<T> randomizer) {
        final MultipleObjects config = getFillableConfigInfo(clasz);
        if (config == null) {
            return new ArrayList<T>(0);
        } else {
            return getList(clasz, randomizer, config.get(Integer.class, 0), config.get(Integer.class, 1));
        }
    }

    private static MultipleObjects getFillableConfigInfo(final Class<?> clasz) {
        final int min, max;
        if (clasz.isAnnotationPresent(RandomFillableConfig.class)) {
            final RandomFillableConfig config = clasz.getAnnotation(RandomFillableConfig.class);
            if (config.ignore()) {
                return null;
            }
            min = config.minLength();
            max = config.maxLength();
        } else {
            min = RandomFillableConfig.MIN_LENGTH;
            max = RandomFillableConfig.MAX_LENGTH;
        }
        return MultipleObjects.fromAll(min, max);
    }

    /**
     * <p>getList.</p>
     *
     * @param <T>   Tipo do objeto.
     * @param clasz Classe do objeto.
     * @return Lista de objetos. classe.
     * @see com.simpou.commons.utils.lang.annot.RandomFillableConfig
     */
    public static <T extends Randomizable> List<T> getList(
            final Class<T> clasz) {
        final MultipleObjects config = getFillableConfigInfo(clasz);
        if (config == null) {
            return new ArrayList<T>(0);
        } else {
            return getList(clasz, config.get(Integer.class, 0), config.get(Integer.class, 1));
        }
    }

    /**
     * <p>getList.</p>
     *
     * @param <T>       Tipo do objeto.
     * @param clasz     Classe do objeto.
     * @param generator Preenchedor randômico.
     * @param min       Número mínimo de elementos.
     * @param max       Número máximo de elementos.
     * @return Lista de objetos. classe.
     */
    public static <T> List<T> getList(
            final Class<T> clasz, final RandGenerator<T> generator, final int min, final int max) {
        final int size = getInteger(min, max);
        final List<T> ts = new ArrayList<T>();

        for (int i = 0; i < size; i++) {
            ts.add(generator.generate());
        }

        return ts;
    }

    /**
     * <p>getList.</p>
     *
     * @param <T>        Tipo do objeto.
     * @param clasz      Classe do objeto.
     * @param randomizer Preenchedor randômico.
     * @param min        Número mínimo de elementos.
     * @param max        Número máximo de elementos.
     * @return Lista de objetos. classe.
     */
    public static <T> List<T> getList(
            final Class<T> clasz, final Randomizer<T> randomizer, final int min, final int max) {

        final int size = getInteger(min, max);
        final List<T> ts = new ArrayList<T>();

        for (int i = 0; i < size; i++) {
            ts.add(getSingle(clasz, randomizer));
        }

        return ts;
    }

    /**
     * <p>getArray.</p>
     *
     * @param <T>       Tipo do objeto.
     * @param clasz     Classe do objeto.
     * @param generator Gerador de aleatórios compatível com o tipo requerido.
     * @param min       Número mínimo de elementos.
     * @param max       Número máximo de elementos.
     * @return Array de objetos.
     */
    public static <T> T[] getArray(
            final Class<T> clasz, final RandGenerator<T> generator, final int min, final int max) {
        int length = getInteger(min, max);
        final T[] ts = Generics.newArray(clasz, length);
        for (int i = 0; i < length; i++) {
            ts[i] = generator.generate();
        }
        return ts;
    }

    /**
     * <p>getArray.</p>
     *
     * @param <T>        Tipo do objeto.
     * @param clasz      Classe do objeto.
     * @param randomizer Randomizador compatível com o tipo requerido.
     * @param min        Número mínimo de elementos.
     * @param max        Número máximo de elementos.
     * @return Array de objetos.
     */
    public static <T> T[] getArray(
            final Class<T> clasz, final Randomizer<T> randomizer, final int min, final int max) {
        int length = getInteger(min, max);
        final T[] ts = Generics.newArray(clasz, length);

        try {
            for (int i = 0; i < length; i++) {
                T newInstance = null;
                newInstance = clasz.newInstance();
                randomizer.fillRandom(newInstance);
                ts[i] = newInstance;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return ts;
    }

    /**
     * <p>getArray.</p>
     *
     * @param <T>   Tipo do objeto.
     * @param clasz Classe do objeto.
     * @param min   Número mínimo de elementos.
     * @param max   Número máximo de elementos.
     * @return Array de objetos.
     */
    public static <T extends Randomizable> T[] getArray(
            final Class<T> clasz, final int min, final int max) {
        int length = getInteger(min, max);
        final T[] ts = Generics.newArray(clasz, length);

        try {
            for (int i = 0; i < length; i++) {
                T newInstance = null;
                newInstance = clasz.newInstance();
                newInstance.fillRandom();
                ts[i] = newInstance;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return ts;
    }

    /**
     * <p>getList.</p>
     *
     * @param <T>        Tipo dos objetos da lista.
     * @param list       Lista.
     * @param allowEmpty Define se a sublista gerada pode ser vazia.
     * @return Sublista.
     */
    public static <T> List<T> getSubList(final List<T> list,
                                         final boolean allowEmpty) {
        if (list == null) {
            throw new IllegalArgumentException("List cannot be null.");
        }

        if (list.isEmpty()) {
            if (allowEmpty) {
                return new ArrayList<T>();
            } else {
                throw new IllegalArgumentException("Empty list not allowed.");
            }
        }

        if ((list.size() < 2) && !allowEmpty) {
            return list;
        }

        List<T> newList = new ArrayList<T>();

        for (T element : list) {
            if (getBoolean()) {
                newList.add(element);
            }
        }

        if (!allowEmpty && newList.isEmpty()) {
            newList.add(list.get(getInteger(0, list.size() - 1)));
        }

        return newList;
    }

    /**
     * <p>getLimits.</p>
     *
     * @param maxElements Número máximo de elementos.
     * @return Limites de paginação.
     */
    public static PageLimits getLimits(final int maxElements) {
        if (maxElements < 1) {
            throw new IllegalArgumentException("Max must be positive.");
        }

        int offset = getInteger(0, maxElements - 1);
        int size = getInteger(1, Math.max(maxElements / 2, 1));

        size = Math.min(offset + size, maxElements - offset);

        PageLimits limits = new PageLimits(offset, size);

        return limits;
    }

    /**
     * <p>getLocale.</p>
     *
     * @return Locale.
     */
    public static String getLocale() {
        Locale loc = getSingle(Locale.getAvailableLocales());

        return loc.toString();
    }

    /**
     * @param max Número máximo.
     * @return Escolhe um número entre 0 e um máximo exclusive. Retorna -1 caso
     *         valor de max menor que um.
     */
    private static int chooseNumber(final int max) {
        int choose;

        if (max < 1) {
            return -1;
        } else if (max < 2) {
            choose = 0;
        } else {
            choose = getInteger(0, max - 1);
        }

        return choose;
    }

    /**
     * Verifica se um valor está dentro dos limites de uma faixa.
     *
     * @param min   Número mínimo de elementos.
     * @param max   Número máximo de elementos.
     * @param value Número de elementos informado.
     */
    private static void verifyMinMax(final int min, final int max,
                                     final int value) {
        if ((value < min) || (value > max)) {
            throw new IllegalArgumentException("Value must be between " + min +
                    " and " + max + " inclusive, but is " + value);
        }
    }
}
